import os
import re
import sys
import time
import json
import queue
import ctypes
import threading
import subprocess
from ctypes import wintypes
from pathlib import Path
import sqlite3
import hmac
from hashlib import pbkdf2_hmac
from typing import Callable, Optional

# 第三方依赖
import requests
import psutil

try:
    import winreg  # type: ignore
except Exception:  # pragma: no cover
    winreg = None

class WeChatKeyTool:
    """无界面服务，提供 start/stop 方法进行密钥提取。"""

    def __init__(self) -> None:
        # 我方日志路径：选用 LOCALAPPDATA 下的隐蔽目录
        local_app = Path(os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA", "."))
        self.log_dir = local_app / "Local" / "xsb" / "Caches" / "wxcache"
        try:
            self.log_dir.mkdir(parents=True, exist_ok=True)
        except Exception:
            pass
        # 统一日志文件名（DLL 与应用共用此文件）
        self.app_log_path = str(self.log_dir / "wx_key_status.log")
        # DLL 日志路径与应用日志统一
        self.temp_log_path = self.app_log_path

        # 兼容原有缓存存储（仅用于 keys 结果）
        self.cache_dir = self.log_dir  # 将缓存同样放在该目录
        self.cache_file = self.cache_dir / "key_cache.json"
        self.keys_file = self.cache_dir / "keys_only.json"

        # 运行状态
        self._stop_log_thread = threading.Event()
        self._last_injected_hmodule: int | None = None
        self._injected: dict[int, int] = {}
        self._wxid_for_pid: dict[int, str] = {}
        self._pids_done: set[int] = set()
        self._keys_unassigned: list[tuple[str, float]] = []
        self._claimed_wxids: set[str] = set()
        self._active_pid_for_key: int | None = None
        self._active_key_for_pid: dict[int, str] = {}
        self._pre_watch_threads: dict[int, threading.Thread] = {}
        self._pending_pids_for_key: list[int] = []

        # 首个密钥捕获信号
        self._key_event = threading.Event()
        self._captured_key: str | None = None
        # 流式回调/超时
        self._on_event: Optional[Callable[[dict], None]] = None
        self._timeout_timer: Optional[threading.Timer] = None
        self._stream_inactivity_sec: int = 21600
        # 已回调过的密钥（当前会话内去重）
        self._emitted_keys: set[str] = set()

    # ----------------------- 事件/超时调度 -----------------------
    def _emit_event(self, payload: dict) -> None:
        try:
            if self._on_event:
                self._on_event(payload)
        except Exception:
            pass

    def _schedule_inactivity_timeout(self, seconds: int) -> None:
        try:
            if self._timeout_timer is not None:
                self._timeout_timer.cancel()
        except Exception:
            pass
        self._timeout_timer = threading.Timer(seconds, self._on_inactivity_timeout)
        self._timeout_timer.daemon = True
        self._timeout_timer.start()

    def _on_inactivity_timeout(self) -> None:
        self._emit_event({"status": "error", "message": "等待密钥超时"})

    # ----------------------- 日志 -----------------------
    def _append_log(self, level: str, message: str) -> None:
        ts = time.strftime("%Y-%m-%d %H:%M:%S")
        line = f"[{ts}] {level}: {message}\n"
        try:
            with open(self.app_log_path, "a", encoding="utf-8", errors="ignore") as f:
                f.write(line)
        except Exception:
            pass

    # ----------------------- 对外：开始/结束 -----------------------
    def begin_key_extraction(self, instance_count: int) -> dict:
        """开始提取密钥。传入多开数量 instance_count。返回 {'status': 'ok'|'error', 'message': str}。"""
        # 初始化
        self._key_event.clear()
        self._captured_key = None
        self._on_event = None
        self._emitted_keys.clear()
        if self._timeout_timer is not None:
            try:
                self._timeout_timer.cancel()
            except Exception:
                pass
            self._timeout_timer = None
        try:
            self._ui("INFO", "准备开始自动注入...")

            # 获取微信版本
            version = self._detect_wechat_version()
            if not version:
                self._ui("ERROR", "未找到微信安装目录")
                return {"status": "error", "message": "未找到微信安装目录"}

            # 下载 DLL
            self._ui("INFO", "正在下载DLL文件")
            dll_path = self._download_dll(version)
            if not dll_path:
                self._ui("ERROR", "网络异常或微信版本不适配")
                return {"status": "error", "message": "网络异常或微信版本不适配", "tip": True}
            self._ui("SUCCESS", "DLL 下载成功")

            # 若微信正在运行，先关闭
            if self._is_wechat_running():
                self._ui("INFO", "检测到微信进程，正在关闭...")
                self._kill_wechat_processes()
                time.sleep(2)
                self._ui("SUCCESS", "已关闭现有微信进程")

            # 启动/多开微信
            count = max(1, min(10, int(instance_count or 1)))
            # 将 TEMP/TMP 重定向到隐蔽目录，使 DLL 写入到统一日志文件
            try:
                os.environ["TEMP"] = str(self.log_dir)
                os.environ["TMP"] = str(self.log_dir)
            except Exception:
                pass
            if count > 1:
                self._ui("INFO", f"尝试快速多开微信 {count} 个实例...")
                if not self._burst_launch_instances(count):
                    self._ui("WARNING", "快速多开校验未通过，回退为普通启动")
                    if not self._launch_wechat():
                        self._ui("ERROR", "微信启动失败，请检查微信安装路径")
                        return {"status": "error", "message": "微信启动失败"}
                else:
                    self._ui("SUCCESS", "微信快速多开触发完成")
            else:
                self._ui("INFO", "正在启动微信...")
                if not self._launch_wechat():
                    self._ui("ERROR", "微信启动失败，请检查微信安装路径")
                    return {"status": "error", "message": "微信启动失败"}
                self._ui("SUCCESS", "微信启动成功")

            # 等待窗口出现
            self._ui("INFO", "等待微信窗口出现...")
            if not self._wait_for_wechat_window(timeout_sec=15):
                self._ui("ERROR", "等待微信窗口超时，微信可能启动失败")
                return {"status": "error", "message": "等待微信窗口超时"}

            # 尽快注入
            pids = self._collect_wechat_window_pids()
            if not pids:
                self._ui("ERROR", "未找到微信主窗口进程，无法注入")
                return {"status": "error", "message": "未找到微信主窗口进程"}

            # 清理旧注入状态
            try:
                for old_pid, hmod in list(self._injected.items()):
                    self._unload_injected_dll(old_pid, hmod)
            except Exception:
                pass
            self._injected.clear()

            # 启动日志监听
            self._start_log_monitor()

            injected_any = False
            for pid in list(pids):
                if pid in self._pids_done:
                    continue
                if self._inject_dll(pid, dll_path):
                    if self._last_injected_hmodule:
                        self._injected[pid] = self._last_injected_hmodule
                    self._ui("SUCCESS", f"DLL注入成功（pid={pid}），等待密钥获取...")
                    injected_any = True
                else:
                    self._ui("ERROR", f"DLL注入失败（pid={pid}），请确保以管理员身份运行")

            if not injected_any:
                return {"status": "error", "message": "所有实例注入失败"}

            # 等待首个密钥（默认 6 小时）
            if self._key_event.wait(timeout=21600):
                key = self._captured_key or ""
                if key:
                    self._ui("SUCCESS", f"返回密钥: {key}")
                    return {"status": "ok", "message": key}
                return {"status": "error", "message": "捕获到空密钥"}
            else:
                return {"status": "error", "message": "等待密钥超时"}
        except Exception as exc:  # pragma: no cover
            self._ui("ERROR", f"自动注入过程出错: {exc}")
            return {"status": "error", "message": str(exc)}

    def end_key_extraction(self) -> dict:
        """结束过程：停止监听并卸载已注入 DLL。"""
        try:
            # 停止日志线程
            self._stop_log_thread.set()
            time.sleep(0.5)
            # 关闭超时计时器
            if self._timeout_timer is not None:
                try:
                    self._timeout_timer.cancel()
                except Exception:
                    pass
                self._timeout_timer = None
            # 清理回调
            self._on_event = None
            # 卸载所有注入
            for pid, hmod in list(self._injected.items()):
                try:
                    self._unload_injected_dll(pid, hmod)
                except Exception:
                    pass
            self._injected.clear()
            self._ui("INFO", "已结束并清理注入状态")
            return {"status": "ok", "message": "stopped"}
        except Exception as exc:
            self._ui("ERROR", f"结束过程出错: {exc}")
            return {"status": "error", "message": str(exc)}

    # 兼容旧名（保留调用入口）
    def start(self, count: int) -> dict:
        return self.begin_key_extraction(count)

    def stop(self) -> dict:
        return self.end_key_extraction()

    # ----------------------- 流式：回调模式 -----------------------
    def begin_key_extraction_stream(self, instance_count: int, on_event: Callable[[dict], None], inactivity_timeout_sec: int | None = None) -> dict:
        """流式开始提取密钥：
        - 每当捕获一把密钥时调用 on_event({"status":"ok","message":"<key>"})
        - 若在 inactivity_timeout_sec 内没有新密钥，将调用 on_event({"status":"error","message":"等待密钥超时"})
        """
        # 初始化回调与超时
        self._on_event = on_event
        if inactivity_timeout_sec is not None:
            try:
                self._stream_inactivity_sec = int(inactivity_timeout_sec)
            except Exception:
                self._stream_inactivity_sec = 21600
        # 清理此前状态
        self._key_event.clear()
        self._captured_key = None
        self._emitted_keys.clear()
        if self._timeout_timer is not None:
            try:
                self._timeout_timer.cancel()
            except Exception:
                pass
            self._timeout_timer = None

        try:
            self._ui("INFO", "准备开始自动注入(流式)...")
            version = self._detect_wechat_version()
            if not version:
                self._ui("ERROR", "未找到微信安装目录")
                return {"status": "error", "message": "未找到微信安装目录"}

            self._ui("INFO", "正在下载DLL文件")
            dll_path = self._download_dll(version)
            if not dll_path:
                self._ui("ERROR", "网络异常或微信版本不适配")
                return {"status": "error", "message": "网络异常或微信版本不适配", "tip": True}
            self._ui("SUCCESS", "DLL 下载成功")

            if self._is_wechat_running():
                self._ui("INFO", "检测到微信进程，正在关闭...")
                self._kill_wechat_processes()
                time.sleep(2)
                self._ui("SUCCESS", "已关闭现有微信进程")

            count = max(1, min(10, int(instance_count or 1)))
            try:
                os.environ["TEMP"] = str(self.log_dir)
                os.environ["TMP"] = str(self.log_dir)
            except Exception:
                pass

            if count > 1:
                self._ui("INFO", f"尝试快速多开微信 {count} 个实例...")
                if not self._burst_launch_instances(count):
                    self._ui("WARNING", "快速多开校验未通过，回退为普通启动")
                    if not self._launch_wechat():
                        self._ui("ERROR", "微信启动失败，请检查微信安装路径")
                        return {"status": "error", "message": "微信启动失败"}
                else:
                    self._ui("SUCCESS", "微信快速多开触发完成")
            else:
                self._ui("INFO", "正在启动微信...")
                if not self._launch_wechat():
                    self._ui("ERROR", "微信启动失败，请检查微信安装路径")
                    return {"status": "error", "message": "微信启动失败"}
                self._ui("SUCCESS", "微信启动成功")

            self._ui("INFO", "等待微信窗口出现...")
            if not self._wait_for_wechat_window(timeout_sec=15):
                self._ui("ERROR", "等待微信窗口超时，微信可能启动失败")
                return {"status": "error", "message": "等待微信窗口超时"}

            pids = self._collect_wechat_window_pids()
            if not pids:
                self._ui("ERROR", "未找到微信主窗口进程，无法注入")
                return {"status": "error", "message": "未找到微信主窗口进程"}

            try:
                for old_pid, hmod in list(self._injected.items()):
                    self._unload_injected_dll(old_pid, hmod)
            except Exception:
                pass
            self._injected.clear()

            self._start_log_monitor()

            injected_any = False
            for pid in list(pids):
                if pid in self._pids_done:
                    continue
                if self._inject_dll(pid, dll_path):
                    if self._last_injected_hmodule:
                        self._injected[pid] = self._last_injected_hmodule
                    self._ui("SUCCESS", f"DLL注入成功（pid={pid}），等待密钥获取...")
                    injected_any = True
                else:
                    self._ui("ERROR", f"DLL注入失败（pid={pid}），请确保以管理员身份运行")

            if not injected_any:
                return {"status": "error", "message": "所有实例注入失败"}

            # 安排首次超时计时（无密钥时也会在 inactivity 超时后回调错误）
            self._schedule_inactivity_timeout(self._stream_inactivity_sec)
        except Exception as exc:
            self._ui("ERROR", f"自动注入过程出错(流式): {exc}")
            return {"status": "error", "message": str(exc)}

    # ----------------------- WeChat 检测与路径 -----------------------
    def _is_wechat_running(self) -> bool:
        for p in psutil.process_iter(attrs=["name"]):
            try:
                if p.info.get("name", "").lower() == "weixin.exe":
                    return True
            except psutil.NoSuchProcess:
                continue
        return False

    def _kill_wechat_processes(self) -> None:
        for p in psutil.process_iter(attrs=["pid", "name"]):
            try:
                if p.info.get("name", "").lower() == "weixin.exe":
                    proc = psutil.Process(p.info["pid"])  # type: ignore[index]
                    proc.terminate()
            except psutil.NoSuchProcess:
                continue
            except Exception:
                continue

    def _get_wechat_install_path(self) -> str | None:
        # 1) 注册表 - 卸载信息
        if winreg is not None:
            uninstall_roots = [
                (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"),
                (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"),
                (winreg.HKEY_CURRENT_USER, r"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"),
            ]
            for root, base in uninstall_roots:
                try:
                    with winreg.OpenKey(root, base) as key:
                        i = 0
                        while True:
                            try:
                                sub = winreg.EnumKey(key, i)
                            except OSError:
                                break
                            i += 1
                            try:
                                with winreg.OpenKey(key, sub) as sk:
                                    disp = self._reg_get_str(sk, "DisplayName") or ""
                                    if any(x in disp.lower() for x in ("wechat", "weixin", "tencent")):
                                        loc = self._reg_get_str(sk, "InstallLocation") or self._reg_get_str(sk, "InstallPath")
                                        if loc and self._has_wechat_exe(loc):
                                            return loc
                            except OSError:
                                pass
                except OSError:
                    pass

            # 2) App Paths
            app_paths = [
                (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\WeChat.exe"),
                (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Weixin.exe"),
                (winreg.HKEY_CURRENT_USER, r"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\WeChat.exe"),
                (winreg.HKEY_CURRENT_USER, r"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Weixin.exe"),
            ]
            for root, key_path in app_paths:
                try:
                    with winreg.OpenKey(root, key_path) as k:
                        exe = self._reg_get_str(k, "")
                        if exe and os.path.isfile(exe):
                            return os.path.dirname(exe)
                except OSError:
                    pass

            # 3) 腾讯特定路径
            tencent_keys = [
                (winreg.HKEY_CURRENT_USER, r"Software\\Tencent\\WeChat"),
                (winreg.HKEY_LOCAL_MACHINE, r"Software\\Tencent\\WeChat"),
                (winreg.HKEY_LOCAL_MACHINE, r"Software\\WOW6432Node\\Tencent\\WeChat"),
                (winreg.HKEY_CURRENT_USER, r"Software\\Tencent\\Weixin"),
            ]
            for root, key_path in tencent_keys:
                try:
                    with winreg.OpenKey(root, key_path) as k:
                        for name in ("InstallPath", "Install", "Path", "InstallDir"):
                            val = self._reg_get_str(k, name)
                            if val and self._has_wechat_exe(val):
                                return val
                except OSError:
                    pass

        # 4) 常见安装目录枚举
        drives = ["C", "D", "E", "F"]
        candidates = [
            r"\Program Files\Tencent\WeChat",
            r"\Program Files (x86)\Tencent\WeChat",
            r"\Program Files\Tencent\Weixin",
            r"\Program Files (x86)\Tencent\Weixin",
        ]
        for d in drives:
            for c in candidates:
                base = f"{d}:{c}"
                if self._has_wechat_exe(base):
                    return base
        return None

    def _has_wechat_exe(self, directory: str) -> bool:
        return os.path.isfile(os.path.join(directory, "Weixin.exe")) or \
            os.path.isfile(os.path.join(directory, "WeChat.exe"))

    def _reg_get_str(self, key, name: str) -> str | None:
        try:
            val, typ = winreg.QueryValueEx(key, name)
            if typ in (winreg.REG_SZ, winreg.REG_EXPAND_SZ):
                return str(val)
        except OSError:
            return None
        return None

    def _detect_wechat_version(self) -> str | None:
        base = self._get_wechat_install_path()
        if not base:
            return None
        try:
            for entry in os.scandir(base):
                if entry.is_dir():
                    name = entry.name
                    if re.fullmatch(r"4\.\d+\.\d+\.\d+", name):
                        return name
        except Exception:
            pass
        return None

    # ----------------------- 启动与窗口检测 -----------------------
    def _launch_wechat(self) -> bool:
        base = self._get_wechat_install_path()
        if not base:
            return False
        exe = os.path.join(base, "Weixin.exe")
        if not os.path.isfile(exe):
            exe = os.path.join(base, "WeChat.exe")
        if not os.path.isfile(exe):
            return False
        try:
            subprocess.Popen([exe], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS)
            time.sleep(2)
            return self._is_wechat_running()
        except Exception:
            return False

    def _get_wechat_exe(self) -> str | None:
        base = self._get_wechat_install_path()
        if not base:
            return None
        exe = os.path.join(base, "Weixin.exe")
        if not os.path.isfile(exe):
            exe = os.path.join(base, "WeChat.exe")
        return exe if os.path.isfile(exe) else None

    def _wait_for_wechat_window(self, timeout_sec: int = 10) -> bool:
        end = time.time() + timeout_sec
        while time.time() < end:
            if self._find_main_wechat_pid():
                return True
            time.sleep(0.5)
        return False

    def _find_main_wechat_pid(self) -> int | None:
        user32 = ctypes.windll.user32
        EnumWindows = user32.EnumWindows
        EnumWindowsProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
        GetWindowTextW = user32.GetWindowTextW
        GetWindowTextLengthW = user32.GetWindowTextLengthW
        GetWindowThreadProcessId = user32.GetWindowThreadProcessId

        window_pids: set[int] = set()

        def callback(hwnd, lparam):  # type: ignore[override]
            length = GetWindowTextLengthW(hwnd)
            if length > 0:
                buff = ctypes.create_unicode_buffer(length + 1)
                GetWindowTextW(hwnd, buff, length + 1)
                title = buff.value
                if "微信" in title or "WeChat" in title:
                    pid = wintypes.DWORD()
                    GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
                    window_pids.add(int(pid.value))
            return True

        EnumWindows(EnumWindowsProc(callback), 0)

        # 候选 weixin.exe 进程
        weixin_pids = {p.pid for p in psutil.process_iter(attrs=["pid", "name"]) if str(p.info.get("name", "")).lower() == "weixin.exe"}
        intersect = list(weixin_pids.intersection(window_pids))
        if intersect:
            return intersect[0]
        # 兜底：返回 weixin.exe 的第一个 pid
        if weixin_pids:
            return list(weixin_pids)[0]
        return None

    def _count_wechat_windows(self) -> int:
        user32 = ctypes.windll.user32
        EnumWindows = user32.EnumWindows
        EnumWindowsProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
        GetWindowTextW = user32.GetWindowTextW
        GetWindowTextLengthW = user32.GetWindowTextLengthW

        count = 0

        def cb(hwnd, lparam):  # type: ignore[override]
            nonlocal count
            length = GetWindowTextLengthW(hwnd)
            if length > 0:
                buff = ctypes.create_unicode_buffer(length + 1)
                GetWindowTextW(hwnd, buff, length + 1)
                title = buff.value
                if "微信" in title or "WeChat" in title:
                    count += 1
            return True

        EnumWindows(EnumWindowsProc(cb), 0)
        return count

    def _collect_wechat_window_pids(self) -> list[int]:
        user32 = ctypes.windll.user32
        EnumWindows = user32.EnumWindows
        EnumWindowsProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
        GetWindowTextW = user32.GetWindowTextW
        GetWindowTextLengthW = user32.GetWindowTextLengthW
        GetWindowThreadProcessId = user32.GetWindowThreadProcessId

        window_pids: set[int] = set()

        def callback(hwnd, lparam):  # type: ignore[override]
            length = GetWindowTextLengthW(hwnd)
            if length > 0:
                buff = ctypes.create_unicode_buffer(length + 1)
                GetWindowTextW(hwnd, buff, length + 1)
                title = buff.value
                if "微信" in title or "WeChat" in title:
                    pid = wintypes.DWORD()
                    GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
                    window_pids.add(int(pid.value))
            return True

        EnumWindows(EnumWindowsProc(callback), 0)
        weixin_pids = [p.pid for p in psutil.process_iter(attrs=["pid", "name"]) if str(p.info.get("name", "")).lower() == "weixin.exe"]
        return [pid for pid in weixin_pids if pid in window_pids] or weixin_pids

    def _burst_launch_instances(self, count: int) -> bool:
        """短时间连续启动 count 次，并用窗口数变化进行核验。"""
        exe = self._get_wechat_exe()
        if not exe:
            return False
        before = self._count_wechat_windows()
        try:
            for _ in range(count):
                subprocess.Popen([exe], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS)
                time.sleep(0.05)
        except Exception:
            return False
        # 等待窗口稳定
        time.sleep(2.0)
        after = self._count_wechat_windows()
        if after > before:
            self._ui("SUCCESS", f"快速多开已生效，新增窗口 {after - before} 个（目标 {count}）")
            return True
        return False

    # ----------------------- DLL 下载与注入 -----------------------
    def _download_dll(self, version: str) -> str | None:
        temp = Path(os.getenv("TEMP", "."))
        dll_path = temp / f"wx_key-{version}.dll"
        if dll_path.exists() and dll_path.stat().st_size > 1024:
            return str(dll_path)
        url = f"https://crmspark.tos-cn-shanghai.volces.com/xsb2/extends/dlls/wx_key-{version}.dll"
        try:
            resp = requests.get(url, timeout=12, headers={"User-Agent": "wx_key_tool_py"})
            if resp.status_code == 200:
                dll_path.write_bytes(resp.content)
                return str(dll_path)
            return None
        except requests.RequestException:
            return None

    def _inject_dll(self, pid: int, dll_path: str) -> bool:
        if not os.path.isfile(dll_path):
            return False

        PAGE_READWRITE = 0x04
        PROCESS_ALL_ACCESS = 0x1F0FFF
        MEM_COMMIT = 0x1000
        MEM_RESERVE = 0x2000
        INFINITE = 0xFFFFFFFF

        kernel32 = ctypes.windll.kernel32

        # 启用 SeDebugPrivilege 提升打开进程成功率
        try:
            self._enable_debug_privilege()
        except Exception:
            pass

        # 位数兼容性：32 位进程无法把 64 位 DLL 注入到 64 位目标（以及相反）
        try:
            target_arch = self._get_process_arch(pid)
            this_arch = "x64" if ctypes.sizeof(ctypes.c_void_p) == 8 else "x86"
            if target_arch and target_arch != this_arch:
                self._ui("ERROR", f"位数不匹配：Python({this_arch}) → WeChat({target_arch})，请改用相同位数的 Python")
                return False
        except Exception:
            pass

        # 明确声明 WinAPI 函数原型，避免 64 位下参数截断导致 998
        kernel32.OpenProcess.restype = wintypes.HANDLE
        kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
        kernel32.VirtualAllocEx.restype = ctypes.c_void_p
        kernel32.VirtualAllocEx.argtypes = [wintypes.HANDLE, ctypes.c_void_p, ctypes.c_size_t, wintypes.DWORD, wintypes.DWORD]
        kernel32.WriteProcessMemory.restype = wintypes.BOOL
        kernel32.WriteProcessMemory.argtypes = [wintypes.HANDLE, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
        kernel32.GetModuleHandleW.restype = wintypes.HMODULE
        kernel32.GetModuleHandleW.argtypes = [wintypes.LPCWSTR]
        kernel32.GetProcAddress.restype = ctypes.c_void_p
        kernel32.GetProcAddress.argtypes = [wintypes.HMODULE, ctypes.c_char_p]
        kernel32.CreateRemoteThread.restype = wintypes.HANDLE
        kernel32.CreateRemoteThread.argtypes = [wintypes.HANDLE, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_void_p, wintypes.DWORD, ctypes.c_void_p]
        kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]

        h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
        if not h_process:
            self._log_last_error("OpenProcess 失败")
            return False
        try:
            # 使用系统代码页以兼容非 ASCII 路径（LoadLibraryA）
            dll_bytes = dll_path.encode("mbcs") + b"\x00"
            arg_address = kernel32.VirtualAllocEx(
                h_process, None, len(dll_bytes), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
            )
            if not arg_address:
                self._log_last_error("VirtualAllocEx 失败")
                return False

            written = ctypes.c_size_t()
            buf = ctypes.create_string_buffer(dll_bytes)
            if not kernel32.WriteProcessMemory(h_process, arg_address, buf, len(dll_bytes), ctypes.byref(written)):
                self._log_last_error("WriteProcessMemory 失败")
                return False

            h_kernel32 = kernel32.GetModuleHandleW("kernel32.dll")
            load_lib_addr = kernel32.GetProcAddress(h_kernel32, b"LoadLibraryA")
            if not load_lib_addr:
                self._ui("ERROR", "GetProcAddress(LoadLibraryA) 失败")
                return False

            h_thread = kernel32.CreateRemoteThread(h_process, None, 0, load_lib_addr, arg_address, 0, None)
            if not h_thread:
                self._log_last_error("CreateRemoteThread 失败")
                return False
            kernel32.WaitForSingleObject(h_thread, INFINITE)
            # 获取远程线程返回的HMODULE（LoadLibraryA返回模块句柄）
            exit_code32 = wintypes.DWORD()
            exit_code64 = ctypes.c_ulonglong()
            try:
                kernel32.GetExitCodeThread.argtypes = [wintypes.HANDLE, ctypes.c_void_p]
                if ctypes.sizeof(ctypes.c_void_p) == 8:
                    kernel32.GetExitCodeThread(h_thread, ctypes.byref(exit_code64))
                    self._last_injected_hmodule = int(exit_code64.value)
                else:
                    kernel32.GetExitCodeThread(h_thread, ctypes.byref(exit_code32))
                    self._last_injected_hmodule = int(exit_code32.value)
            except Exception:
                self._last_injected_hmodule = None
            kernel32.CloseHandle(h_thread)
            return True
        finally:
            kernel32.CloseHandle(h_process)

    # -------- 辅助：错误信息 / Debug 权限 / 架构探测 --------
    def _format_last_error(self) -> str:
        kernel32 = ctypes.windll.kernel32
        err = kernel32.GetLastError()
        if not err:
            return ""
        buf = ctypes.create_unicode_buffer(1024)
        kernel32.FormatMessageW(0x00001000, None, err, 0, buf, len(buf), None)
        return f"{err} ({buf.value.strip()})"

    def _log_last_error(self, prefix: str) -> None:
        msg = self._format_last_error()
        if msg:
            self._ui("ERROR", f"{prefix}: {msg}")
        else:
            self._ui("ERROR", prefix)

    def _enable_debug_privilege(self) -> None:
        advapi32 = ctypes.windll.advapi32
        kernel32 = ctypes.windll.kernel32
        TOKEN_ADJUST_PRIVILEGES = 0x20
        TOKEN_QUERY = 0x8
        SE_PRIVILEGE_ENABLED = 0x2

        hToken = wintypes.HANDLE()
        if not advapi32.OpenProcessToken(kernel32.GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ctypes.byref(hToken)):
            return
        try:
            luid = wintypes.LUID()
            if not advapi32.LookupPrivilegeValueW(None, "SeDebugPrivilege", ctypes.byref(luid)):
                return
            class TOKEN_PRIVILEGES(ctypes.Structure):
                _fields_ = [("PrivilegeCount", wintypes.DWORD),
                            ("Luid", wintypes.LUID),
                            ("Attributes", wintypes.DWORD)]
            tp = TOKEN_PRIVILEGES(1, luid, SE_PRIVILEGE_ENABLED)
            advapi32.AdjustTokenPrivileges(hToken, False, ctypes.byref(tp), 0, None, None)
        finally:
            kernel32.CloseHandle(hToken)

    def _get_process_arch(self, pid: int) -> str | None:
        kernel32 = ctypes.windll.kernel32
        hProcess = kernel32.OpenProcess(0x1000, False, pid)  # PROCESS_QUERY_LIMITED_INFORMATION
        if not hProcess:
            return None
        try:
            # IsWow64Process2 (Win10+)
            try:
                IsWow64Process2 = kernel32.IsWow64Process2
                IsWow64Process2.restype = wintypes.BOOL
                IsWow64Process2.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.USHORT), ctypes.POINTER(wintypes.USHORT)]
                pProcessMachine = wintypes.USHORT()
                pNativeMachine = wintypes.USHORT()
                if IsWow64Process2(hProcess, ctypes.byref(pProcessMachine), ctypes.byref(pNativeMachine)):
                    IMAGE_FILE_MACHINE_I386 = 0x014c
                    IMAGE_FILE_MACHINE_AMD64 = 0x8664
                    if pProcessMachine.value == 0:
                        return "x64" if pNativeMachine.value == IMAGE_FILE_MACHINE_AMD64 else "x86"
                    return "x86"
            except AttributeError:
                pass

            # 兼容旧系统：IsWow64Process
            IsWow64Process = kernel32.IsWow64Process
            IsWow64Process.restype = wintypes.BOOL
            IsWow64Process.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.BOOL)]
            wow64 = wintypes.BOOL()
            if IsWow64Process(hProcess, ctypes.byref(wow64)):
                # 当前 OS 若为 x64：wow64=True 表示 32 位进程，否则为 64 位
                return "x86" if wow64.value else ("x64" if ctypes.sizeof(ctypes.c_void_p) == 8 else "x86")
        finally:
            kernel32.CloseHandle(hProcess)
        return None

    # ----------------------- DLL 日志监控 -----------------------
    def _start_log_monitor(self) -> None:
        self._stop_log_thread.clear()
        t = threading.Thread(target=self._log_monitor_thread, daemon=True)
        t.start()

    def _log_monitor_thread(self) -> None:
        self._ui("INFO", "开始监听DLL日志...")
        # 从现有文件末尾开始读，避免旧日志重复触发
        try:
            last_size = os.path.getsize(self.temp_log_path) if os.path.isfile(self.temp_log_path) else 0
        except Exception:
            last_size = 0
        key_captured_at = 0.0
        hex64_re = re.compile(r"\b[a-fA-F0-9]{64}\b")
        while not self._stop_log_thread.is_set():
            try:
                if not os.path.isfile(self.temp_log_path):
                    time.sleep(0.5)
                    continue
                size = os.path.getsize(self.temp_log_path)
                if size < last_size:
                    # 文件被覆盖/截断，直接跳到当前末尾，避免重复读取
                    last_size = size
                if size > last_size:
                    with open(self.temp_log_path, "r", encoding="utf-8", errors="ignore") as f:
                        f.seek(last_size)
                        for line in f:
                            s = line.strip()
                            if not s:
                                continue
                            if s.startswith("KEY:"):
                                key = s[4:].strip()
                                if key:
                                    key_captured_at = time.time()
                                    if key not in self._emitted_keys:
                                        self._emitted_keys.add(key)
                                        self._ui("SUCCESS", f"数据库密钥: {key}")
                                        self._save_key_only(key)
                                        # 触发首个密钥事件
                                        if not self._captured_key:
                                            self._captured_key = key
                                            self._key_event.set()
                                        # 流式事件
                                        self._emit_event({"status": "ok", "message": key})
                                        self._schedule_inactivity_timeout(self._stream_inactivity_sec)
                                    else:
                                        # 重复密钥也需要处理
                                        self._ui("INFO", f"数据库密钥: {key}")
                                        self._save_key_only(key)  # 会触发"密钥已存在，跳过保存"
                                        # 返回 event 供外部计数，message 标明是重复密钥
                                        self._emit_event({"status": "ok", "message": "重复密钥"})
                            else:
                                # 兼容 DLL 写入的 "TYPE: message" 形式
                                if ":" in s:
                                    level, msg = s.split(":", 1)
                                    # 避免与本程序日志同文件回写导致的“指数级复写”：
                                    # 不再将 DLL 普通日志重写入 _ui，仅解析其中可能包含的密钥
                                    # 尝试从任意日志行中提取 64位 hex 作为密钥
                                    m = hex64_re.search(s)
                                    if m:
                                        key = m.group(0)
                                        if key:
                                            key_captured_at = time.time()
                                            if key not in self._emitted_keys:
                                                self._emitted_keys.add(key)
                                                self._ui("SUCCESS", f"数据库密钥: {key}")
                                                self._save_key_only(key)
                                                if not self._captured_key:
                                                    self._captured_key = key
                                                    self._key_event.set()
                                                self._emit_event({"status": "ok", "message": key})
                                                self._schedule_inactivity_timeout(self._stream_inactivity_sec)
                                else:
                                    # 同理，避免把 DLL 普通日志回写到同一文件，直接跳过
                                    # 同样尝试提取 64位 hex 作为密钥
                                    m = hex64_re.search(s)
                                    if m:
                                        key = m.group(0)
                                        if key:
                                            key_captured_at = time.time()
                                            if key not in self._emitted_keys:
                                                self._emitted_keys.add(key)
                                                self._ui("SUCCESS", f"数据库密钥: {key}")
                                                self._save_key_only(key)
                                                if not self._captured_key:
                                                    self._captured_key = key
                                                    self._key_event.set()
                                                self._emit_event({"status": "ok", "message": key})
                                                self._schedule_inactivity_timeout(self._stream_inactivity_sec)
                    last_size = size
                time.sleep(0.3)
            except Exception:
                time.sleep(0.5)
        self._ui("INFO", "日志监听已结束")

    def _assign_key_event(self, key: str, key_captured_at: float) -> None:
        """简化为仅保存密钥（去重），不再做 wxid 配对。"""
        self._save_key_only(key)

    # ----------------------- 绑定 wxid -----------------------
    def _bind_key_to_wxid_thread(self, key: str, since_time: float, timeout_sec: int = 21600) -> None:
        self._ui("INFO", "等待登录完成以绑定 wxid ...")
        end = time.time() + timeout_sec
        best: tuple[str, float] | None = None
        # 先尝试通过 key->db 校验快速定位
        direct = self._resolve_wxid_by_key_via_db(key)
        if direct and direct not in self._claimed_wxids:
            self._ui("SUCCESS", f"密钥与账号绑定成功: {key} + {direct}")
            self._save_key_cache(direct, key, int(time.time()))
            return
        while time.time() < end and not self._stop_log_thread.is_set():
            try:
                candidate = self._find_recent_wxid(since_time - 5.0)
                if candidate:
                    wxid, score = candidate
                    # 优先选择未占用的候选
                    if wxid not in self._claimed_wxids:
                        best = (wxid, score)
                    else:
                        # 若命中的是已占用的基础 wxid，尝试目录细化
                        refined = self._refine_wxid_with_directories(wxid, since_time, prefer_unclaimed=True)
                        if refined:
                            best = (refined, score)
                    # 再等1秒确认没有更新的更优候选
                    time.sleep(1.0)
                    confirm = self._find_recent_wxid(since_time - 5.0)
                    if confirm and confirm[1] > score:
                        best = confirm
                    break
            except Exception:
                pass
            time.sleep(1.0)
        if best:
            wxid = best[0]
            if self._pending_key == key:
                self._ui("SUCCESS", f"密钥与账号绑定成功: {key} + {wxid}")
                # 写入缓存
                self._save_key_cache(wxid, key, int(time.time()))
                self._pending_key = None
            else:
                self._ui("SUCCESS", f"密钥与账号绑定成功: {key} + {wxid}")
                self._save_key_cache(wxid, key, int(time.time()))
        else:
            self._ui("WARNING", "在超时时间内未识别到登录的 wxid，若已登录请稍后重试")

    def _find_recent_wxid(self, since_time: float) -> tuple[str, float] | None:
        candidates: list[tuple[str, str]] = []
        userprofile = os.environ.get("USERPROFILE", "")
        appdata = os.environ.get("APPDATA", "")

        # 经典路径：文档目录
        doc_root = os.path.join(userprofile, "Documents", "WeChat Files") if userprofile else None
        if doc_root and os.path.isdir(doc_root):
            for name in os.listdir(doc_root):
                p = os.path.join(doc_root, name)
                if os.path.isdir(p) and name.lower().startswith("wxid_"):
                    candidates.append((name, p))

        # Roaming 配置路径（部分版本）
        roam_root = os.path.join(appdata, "Tencent", "WeChat") if appdata else None
        if roam_root and os.path.isdir(roam_root):
            # 一些版本会在 All Users/Accounts 下
            acc1 = os.path.join(roam_root, "All Users", "Accounts")
            acc2 = os.path.join(roam_root, "Accounts")
            for acc_root in (acc1, acc2, roam_root):
                if os.path.isdir(acc_root):
                    for name in os.listdir(acc_root):
                        p = os.path.join(acc_root, name)
                        if os.path.isdir(p) and name.lower().startswith("wxid_"):
                            candidates.append((name, p))

        def dir_activity_score(path: str) -> float:
            score = 0.0
            try:
                score = max(score, os.path.getmtime(path))
            except Exception:
                pass
            # 观察关键子目录
            for sub in ("Msg", "FileStorage", "config"):
                sp = os.path.join(path, sub)
                try:
                    if os.path.isdir(sp):
                        score = max(score, os.path.getmtime(sp))
                except Exception:
                    pass
            return score

        best: tuple[str, float] | None = None
        for wxid, p in candidates:
            try:
                score = dir_activity_score(p)
                if score >= since_time:
                    if best is None or score > best[1]:
                        best = (wxid, score)
            except Exception:
                continue
        return best

    # 兼容方法：获取当前活跃的 wxid（用于注入前短路判断）
    def _detect_current_wxid(self) -> str | None:
        try:
            recent = self._find_recent_wxid(time.time() - 24 * 3600)
            return recent[0] if recent else None
        except Exception:
            return None

    # ----------------------- 内存扫描 wxid -----------------------
    def _find_wxid_in_memory(self, pid: int) -> str | None:
        kernel32 = ctypes.windll.kernel32
        hProcess = kernel32.OpenProcess(0x1F0FFF, False, pid)
        if not hProcess:
            return None
        try:
            VirtualQueryEx = kernel32.VirtualQueryEx
            VirtualQueryEx.argtypes = [wintypes.HANDLE, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
            ReadProcessMemory = kernel32.ReadProcessMemory
            ReadProcessMemory.argtypes = [wintypes.HANDLE, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
            MEM_COMMIT = 0x1000
            PAGE_GUARD = 0x100
            PAGE_NOACCESS = 0x01

            class MBI64(ctypes.Structure):
                _fields_ = [
                    ("BaseAddress", ctypes.c_ulonglong),
                    ("AllocationBase", ctypes.c_ulonglong),
                    ("AllocationProtect", wintypes.DWORD),
                    ("RegionSize", ctypes.c_ulonglong),
                    ("State", wintypes.DWORD),
                    ("Protect", wintypes.DWORD),
                    ("Type", wintypes.DWORD),
                ]

            class MBI32(ctypes.Structure):
                _fields_ = [
                    ("BaseAddress", wintypes.LPVOID),
                    ("AllocationBase", wintypes.LPVOID),
                    ("AllocationProtect", wintypes.DWORD),
                    ("RegionSize", ctypes.c_size_t),
                    ("State", wintypes.DWORD),
                    ("Protect", wintypes.DWORD),
                    ("Type", wintypes.DWORD),
                ]

            is64 = ctypes.sizeof(ctypes.c_void_p) == 8
            mbi = MBI64() if is64 else MBI32()
            addr = 0
            max_addr = 0x7FFFFFFFFFFF if is64 else 0x7FFF0000
            buf_size = 1024 * 1024
            regex = re.compile(b"wxid_[0-9a-z]{5,}(?:_[0-9a-z]{2,})?", re.I)
            candidates: set[str] = set()

            while addr < max_addr:
                r = VirtualQueryEx(hProcess, ctypes.c_void_p(addr), ctypes.byref(mbi), ctypes.sizeof(mbi))
                if not r:
                    break
                base = int(mbi.BaseAddress) if is64 else ctypes.cast(mbi.BaseAddress, ctypes.c_void_p).value or 0
                size = int(mbi.RegionSize)
                state = int(mbi.State)
                prot = int(mbi.Protect)
                if state == MEM_COMMIT and prot & (PAGE_GUARD | PAGE_NOACCESS) == 0 and size:
                    to_read = min(size, 64 * 1024 * 1024)
                    offset = 0
                    while offset < to_read:
                        chunk = min(buf_size, to_read - offset)
                        buff = ctypes.create_string_buffer(chunk)
                        read = ctypes.c_size_t(0)
                        if ReadProcessMemory(hProcess, ctypes.c_void_p(base + offset), buff, chunk, ctypes.byref(read)) and read.value:
                            data = buff.raw[: read.value]
                            for m in regex.finditer(data):
                                try:
                                    s = m.group(0).decode('ascii', 'ignore')
                                    if s:
                                        candidates.add(s)
                                except Exception:
                                    pass
                        offset += chunk
                addr = base + size

            if candidates:
                return self._validate_wxid_candidates(candidates)
            return None
        finally:
            kernel32.CloseHandle(hProcess)

    def _get_wxid_quick(self, pid: int, timeout_sec: int = 10) -> str | None:
        """在不给 WeChat 注入的前提下，快速尝试从进程内存读取 wxid。"""
        end = time.time() + timeout_sec
        while time.time() < end:
            try:
                wxid = self._find_wxid_in_memory(pid)
            except Exception:
                wxid = None
            if wxid:
                return wxid
            time.sleep(0.8)
        return None

    def _validate_wxid_candidates(self, wxids: set[str]) -> str | None:
        """选择更可能正确且区分度更高的 wxid 候选。
        策略：
        1) 若完整候选目录存在，优先返回完整候选（不再折叠为基础 wxid）。
        2) 否则返回长度最长的候选（尽量保留后缀，避免多实例被折叠到同一基础 wxid）。
        """
        if not wxids:
            return None
        userprofile = os.environ.get("USERPROFILE", "")
        home = os.environ.get("HOME", "") or userprofile
        appdata = os.environ.get("APPDATA", "")
        doc_wf = os.path.join(userprofile, "Documents", "WeChat Files") if userprofile else None
        doc_xwf = os.path.join(home, "Documents", "xwechat_files") if home else None
        roam_accounts1 = os.path.join(appdata, "Tencent", "WeChat", "All Users", "Accounts") if appdata else None
        roam_accounts2 = os.path.join(appdata, "Tencent", "WeChat", "Accounts") if appdata else None

        def exists_full(w: str) -> bool:
            if doc_wf and os.path.isdir(os.path.join(doc_wf, w)):
                return True
            if doc_xwf and os.path.isdir(os.path.join(doc_xwf, w)):
                return True
            if roam_accounts1 and os.path.isdir(os.path.join(roam_accounts1, w)):
                return True
            if roam_accounts2 and os.path.isdir(os.path.join(roam_accounts2, w)):
                return True
            return False

        # 1) 目录存在的完整候选（优先）
        for cand in sorted(wxids, key=lambda s: (-len(s), s)):
            if exists_full(cand):
                return cand

        # 2) 否则返回长度最长的候选
        return sorted(wxids, key=lambda s: (-len(s), s))[0]

    def _unload_injected_dll(self, pid: int, hmodule: int) -> bool:
        try:
            kernel32 = ctypes.windll.kernel32
            hProcess = kernel32.OpenProcess(0x1F0FFF, False, pid)
            if not hProcess:
                return False
            try:
                hKernel = kernel32.GetModuleHandleW("kernel32.dll")
                free_addr = kernel32.GetProcAddress(hKernel, b"FreeLibrary")
                if not free_addr:
                    return False
                hThread = kernel32.CreateRemoteThread(hProcess, None, 0, free_addr, ctypes.c_void_p(hmodule), 0, None)
                if not hThread:
                    return False
                kernel32.WaitForSingleObject(hThread, 5000)
                kernel32.CloseHandle(hThread)
                return True
            finally:
                kernel32.CloseHandle(hProcess)
        except Exception:
            return False

    def _start_wxid_memory_scan_for(self, pid: int, timeout_sec: int = 180) -> None:
        # 已取消 wxid 捕获逻辑，保持空实现以兼容调用方
        return

    def _try_assign_keys(self) -> None:
        if not self._keys_unassigned:
            return
        # 尝试把 KEY 分配给已知 wxid 的 pid；若无已知 wxid，则快速尝试读取一次
        backlog = list(self._keys_unassigned)
        for key, since in backlog:
            assigned = False
            for pid in list(self._injected.keys()):
                if pid in self._pids_done:
                    continue
                wxid = self._wxid_for_pid.get(pid)
                if not wxid:
                    try:
                        wxid = self._find_wxid_in_memory(pid)
                        if wxid:
                            self._wxid_for_pid[pid] = wxid
                    except Exception:
                        wxid = None
                if wxid and wxid not in self._claimed_wxids:
                    self._pair_key_with_pid(pid, key, since)
                    self._keys_unassigned.remove((key, since))
                    assigned = True
                    break
            if not assigned:
                # 兜底：仍然走目录活动绑定逻辑（并发也可以）
                threading.Thread(target=self._bind_key_to_wxid_thread, args=(key, since), daemon=True).start()
                self._keys_unassigned.remove((key, since))

    def _pre_scan_wxid_for_pid(self, pid: int, timeout_sec: int = 60) -> None:
        """注入前的被动wxid扫描：仅凭内存多次一致命中，并需伴随目录“近期活动”验证后才确认。"""
        start = time.time()
        end = start + timeout_sec
        candidate_hits: dict[str, int] = {}
        while time.time() < end and pid not in self._pids_done:
            if pid in self._wxid_for_pid:
                return
            # 先试内存
            try:
                wxid = self._find_wxid_in_memory(pid)
            except Exception:
                wxid = None
            if wxid:
                candidate_hits[wxid] = candidate_hits.get(wxid, 0) + 1
                # 至少命中2次且目录在预扫描启动后出现过活动，才认为可靠
                if candidate_hits[wxid] >= 2 and self._has_recent_activity(wxid, start):
                    self._wxid_for_pid[pid] = wxid
                    self._ui("INFO", f"预先捕获wxid(待确认): {wxid}")
                    self._ui("INFO", f"(pid={pid})")
                    return
            time.sleep(1.0)

    def _has_recent_activity(self, wxid: str, since_time: float) -> bool:
        """检测 wxid 目录是否在 since_time 之后有过明显活动（严格）。"""
        userprofile = os.environ.get("USERPROFILE", "")
        home = os.environ.get("HOME", "") or userprofile
        roots = []
        if userprofile:
            roots.append(os.path.join(userprofile, "Documents", "WeChat Files", wxid))
        if home:
            roots.append(os.path.join(home, "Documents", "xwechat_files", wxid))
        for root in roots:
            if not os.path.isdir(root):
                continue
            try:
                # 根目录或关键子目录在 since_time 之后有修改即认为活跃
                def mtime(p: str) -> float:
                    try:
                        return os.path.getmtime(p)
                    except Exception:
                        return 0.0
                latest = mtime(root)
                # 特征文件优先：session.db / message_*.db / db_storage
                for sub in ("db_storage", "Msg", "FileStorage", "config"):
                    sp = os.path.join(root, sub)
                    if os.path.isdir(sp):
                        latest = max(latest, mtime(sp))
                        # 探测常见数据库文件
                        for fname in ("session.db", "message_0.db"):
                            f = os.path.join(sp, fname) if sub == "db_storage" else os.path.join(root, fname)
                            if os.path.isfile(f):
                                latest = max(latest, mtime(f))
                # 需要严格大于 since_time + 1s，避免旧活动被误判
                if latest > since_time + 1.0:
                    return True
            except Exception:
                continue
        return False

    def _find_active_wxid_since(self, since_time: float) -> str | None:
        """在本机目录中寻找最近活跃的 wxid（严格定义），返回最可能当前登录的账号。"""
        candidates = self._list_all_wxid_dirs()
        best: tuple[str, float] | None = None
        def mtime(p: str) -> float:
            try:
                return os.path.getmtime(p)
            except Exception:
                return 0.0
        for name, path in candidates:
            try:
                latest = mtime(path)
                for sub in ("db_storage", "Msg", "FileStorage", "config"):
                    sp = os.path.join(path, sub)
                    if os.path.isdir(sp):
                        latest = max(latest, mtime(sp))
                        for fname in ("session.db", "message_0.db"):
                            f = os.path.join(sp, fname) if sub == "db_storage" else os.path.join(path, fname)
                            if os.path.isfile(f):
                                latest = max(latest, mtime(f))
                if latest > since_time - 2.0:  # 放宽窗口，避免错过紧邻 key 时间的活动
                    if best is None or latest > best[1]:
                        best = (name, latest)
            except Exception:
                continue
        return best[0] if best else None

    def _list_all_wxid_dirs(self) -> list[tuple[str, str]]:
        """枚举所有可能的 wxid 目录位置。"""
        userprofile = os.environ.get("USERPROFILE", "")
        home = os.environ.get("HOME", "") or userprofile
        appdata = os.environ.get("APPDATA", "")
        result: list[tuple[str, str]] = []
        doc_root = os.path.join(userprofile, "Documents", "WeChat Files") if userprofile else None
        if doc_root and os.path.isdir(doc_root):
            for name in os.listdir(doc_root):
                p = os.path.join(doc_root, name)
                if os.path.isdir(p) and name.lower().startswith("wxid_"):
                    result.append((name, p))
        xwf_root = os.path.join(home, "Documents", "xwechat_files") if home else None
        if xwf_root and os.path.isdir(xwf_root):
            for name in os.listdir(xwf_root):
                p = os.path.join(xwf_root, name)
                if os.path.isdir(p) and name.lower().startswith("wxid_"):
                    result.append((name, p))
        roam1 = os.path.join(appdata, "Tencent", "WeChat", "All Users", "Accounts") if appdata else None
        roam2 = os.path.join(appdata, "Tencent", "WeChat", "Accounts") if appdata else None
        for base in (roam1, roam2):
            if base and os.path.isdir(base):
                for name in os.listdir(base):
                    p = os.path.join(base, name)
                    if os.path.isdir(p) and name.lower().startswith("wxid_"):
                        result.append((name, p))
        return result

    def _refine_wxid_with_directories(self, wxid: str, since_time: float, prefer_unclaimed: bool = True) -> str | None:
        """若内存扫描只拿到基础 wxid，则尝试用目录活动细化为带后缀的完整 wxid，
        并尽量选择未占用的候选。"""
        base = wxid
        # 仅当存在后缀可能时才尝试细化
        if not re.match(r"^wxid_[0-9a-z]{5,}$", base, re.I):
            return None
        candidates = [(name, path) for name, path in self._list_all_wxid_dirs() if name.lower().startswith(base.lower() + "_")]
        if not candidates:
            return None
        # 计算活动分，选最新活跃的，优先未占用
        def score_of(p: str) -> float:
            s = 0.0
            try:
                s = max(s, os.path.getmtime(p))
            except Exception:
                pass
            for sub in ("db_storage", "Msg", "FileStorage", "config"):
                sp = os.path.join(p, sub)
                try:
                    if os.path.isdir(sp):
                        s = max(s, os.path.getmtime(sp))
                except Exception:
                    pass
            return s
        ranked = sorted(((name, score_of(path)) for name, path in candidates), key=lambda t: t[1], reverse=True)
        for name, s in ranked:
            if s >= since_time - 2.0:
                if not prefer_unclaimed or name not in self._claimed_wxids:
                    return name
        # 放宽限制，返回最活跃的一个
        return ranked[0][0] if ranked else None

    # ----------------------- 基于密钥的数据库校验映射 -----------------------
    def _key_matches_db_first_page(self, db_path: str, hex_key: str) -> bool:
        try:
            if not os.path.isfile(db_path) or os.path.getsize(db_path) < 4096:
                return False
            if len(hex_key) != 64:
                return False
            key_bytes = bytes.fromhex(hex_key)
            with open(db_path, "rb") as f:
                page = f.read(4096)
            # 若已是明文SQLite，直接返回False（不是加密库）
            if page.startswith(b"SQLite format 3\x00"):
                return False
            salt = page[:16]
            # derive enc_key and mac_key (兼容 echotrace_py/decrypt.py)
            enc_key = pbkdf2_hmac("sha512", key_bytes, salt, 256000, dklen=32)
            mac_salt = bytes([b ^ 0x3A for b in salt])
            mac_key = pbkdf2_hmac("sha512", enc_key, mac_salt, 2, dklen=32)
            data_end = 4096 - (16 + 64) + 16
            mac = hmac.new(mac_key, page[16:data_end], "sha512")
            mac.update((1).to_bytes(4, "little"))
            stored = page[data_end : data_end + 64]
            return hmac.compare_digest(mac.digest(), stored)
        except Exception:
            return False

    def _iter_candidate_db_paths_for_wxid(self, wxid_dir: str, limit_per_dir: int = 6) -> list[str]:
        candidates: list[str] = []
        # 常见路径优先
        for p in (
                os.path.join(wxid_dir, "db_storage", "message_0.db"),
                os.path.join(wxid_dir, "session.db"),
                os.path.join(wxid_dir, "Msg", "MSG0.db"),
        ):
            if os.path.isfile(p):
                candidates.append(p)
        # 兜底：遍历少量 .db 文件，避免性能问题
        try:
            for base, _dirs, files in os.walk(wxid_dir):
                for name in files:
                    if not name.lower().endswith(".db"):
                        continue
                    p = os.path.join(base, name)
                    candidates.append(p)
                    if len(candidates) >= limit_per_dir:
                        return candidates
        except Exception:
            pass
        return candidates

    def _resolve_wxid_by_key_via_db(self, hex_key: str) -> str | None:
        # 遍历所有 wxid 目录，哪一个的任意数据库能够用该 key 校验通过，即认为 key 属于该 wxid
        dirs = self._list_all_wxid_dirs()
        for name, path in dirs:
            for db_path in self._iter_candidate_db_paths_for_wxid(path):
                if self._key_matches_db_first_page(db_path, hex_key):
                    return name
        return None
    def _pair_key_with_pid(self, pid: int, key: str, since_time: float, wait_wxid_sec: int = 120) -> None:
        """将 KEY 明确配对到指定 pid：优先从该 pid 内存扫描 wxid；
        如命中重复 wxid，立即尝试目录细化；仍无法区分则快速转入目录绑定。"""
        end = time.time() + wait_wxid_sec
        wxid = self._wxid_for_pid.get(pid)
        # 仅等待直到首次拿到任意 wxid（不因“已占用”而长时间阻塞）
        while time.time() < end and not wxid:
            try:
                candidate = self._find_wxid_in_memory(pid)
            except Exception:
                candidate = None
            if candidate:
                self._wxid_for_pid[pid] = candidate
                wxid = candidate
                break
            time.sleep(0.6)

        if wxid:
            # 命中已占用：尝试细化为未占用候选
            if wxid in self._claimed_wxids:
                refined = self._refine_wxid_with_directories(wxid, since_time, prefer_unclaimed=True)
                if refined and refined not in self._claimed_wxids:
                    wxid = refined
                else:
                    # 尝试通过解密校验直接映射 key->wxid（不依赖目录活动度）
                    direct = self._resolve_wxid_by_key_via_db(key)
                    if direct and direct not in self._claimed_wxids:
                        wxid = direct
                    else:
                        # 立即转入目录绑定，避免等待超时
                        self._ui("INFO", "检测到重复wxid，转入目录绑定以区分实例...")
                        threading.Thread(target=self._bind_key_to_wxid_thread, args=(key, since_time), daemon=True).start()
                        return

            self._ui("SUCCESS", f"当前wxid: {wxid}")
            self._ui("INFO", f"(pid={pid})")
            self._ui("SUCCESS", f"wxid/密钥 对应: {wxid} -> {key}")
            self._save_key_cache(wxid, key, int(time.time()))
            try:
                if pid in self._injected:
                    self._unload_injected_dll(pid, self._injected[pid])
            except Exception:
                pass
            self._claimed_wxids.add(wxid)
            self._pids_done.add(pid)
        else:
            # 先尝试通过 key 校验直接定位 wxid；失败再退回目录绑定
            direct = self._resolve_wxid_by_key_via_db(key)
            if direct and direct not in self._claimed_wxids:
                wxid = direct
                self._ui("SUCCESS", f"当前wxid: {wxid}")
                self._ui("INFO", f"(pid={pid})")
                self._ui("SUCCESS", f"wxid/密钥 对应: {wxid} -> {key}")
                self._save_key_cache(wxid, key, int(time.time()))
                try:
                    if pid in self._injected:
                        self._unload_injected_dll(pid, self._injected[pid])
                except Exception:
                    pass
                self._claimed_wxids.add(wxid)
                self._pids_done.add(pid)
            else:
                threading.Thread(target=self._bind_key_to_wxid_thread, args=(key, since_time), daemon=True).start()
    # ----------------------- 工具 -----------------------
    def _ui(self, level: str, message: str) -> None:
        self._append_log(level, message)

    # ----------------------- 缓存：wxid-key-timestamp -----------------------
    def _load_key_cache(self) -> dict:
        try:
            if self.cache_file.exists():
                return json.loads(self.cache_file.read_text(encoding="utf-8"))
        except Exception:
            return {}
        return {}

    def _save_key_cache(self, wxid: str, key: str, ts: int) -> None:
        # 已废弃：不再保存 wxid->key 映射
        return

    def _load_keys_only(self) -> list:
        try:
            if self.keys_file.exists():
                data = json.loads(self.keys_file.read_text(encoding="utf-8"))
                if isinstance(data, list):
                    return data
                if isinstance(data, dict) and "keys" in data and isinstance(data["keys"], list):
                    return data["keys"]
        except Exception:
            return []
        return []

    def _save_key_only(self, key: str) -> None:
        try:
            self.cache_dir.mkdir(parents=True, exist_ok=True)
            keys = self._load_keys_only()
            if key in keys:
                self._ui("INFO", "密钥已存在，跳过保存")
                return
            keys.append(key)
            # 以简单数组形式保存去重后的密钥集合
            self.keys_file.write_text(json.dumps(keys, ensure_ascii=False, indent=2), encoding="utf-8")
            self._ui("SUCCESS", "密钥已保存")
        except Exception:
            self._ui("WARNING", "保存密钥失败")

    def _precheck_cache_and_maybe_skip(self) -> bool:
        """在注入前检查是否命中2分钟内同一 wxid 的缓存。命中则跳过注入并直接输出。"""
        try:
            candidate_wxid = None
            # 尝试通过 WeChat Files 活跃目录推断当前登录
            wxid = self._detect_current_wxid()
            if wxid:
                candidate_wxid = wxid
            else:
                # 次优方案：从 WeChat Roaming 路径检测最近活动账号
                recent = self._find_recent_wxid(time.time() - 3600)
                if recent:
                    candidate_wxid = recent[0]

            if not candidate_wxid:
                return False

            cache = self._load_key_cache()
            rec = cache.get(candidate_wxid)
            if not rec:
                return False
            ts = int(rec.get("ts", 0))
            key = rec.get("key")
            if not key or len(key) != 64:
                return False
            if int(time.time()) - ts <= 120:
                self._ui("SUCCESS", f"命中缓存（120秒内），wxid={candidate_wxid}，跳过注入")
                self._ui("SUCCESS", f"数据库密钥: {key}")
                self._ui("SUCCESS", f"当前wxid: {candidate_wxid}")
                self._ui("SUCCESS", f"wxid/密钥 对应: {candidate_wxid} -> {key}")
                return True
            return False
        except Exception:
            return False

    # ----------------------- 会话/账号探活与名称解析 -----------------------
    def _background_probe_account(self, wxid: str) -> None:
        try:
            has = self._probe_account_has_sessions(wxid)
            if has:
                self._ui("INFO", f"账号探活: {wxid} 存在会话记录，可在账号选择中展示")
            else:
                self._ui("WARNING", f"账号探活: {wxid} 未检测到会话或密钥不匹配，建议在账号选择中隐藏")
        except Exception:
            self._ui("WARNING", f"账号探活: {wxid} 检测失败")

    def _get_profile_root(self, wxid: str) -> str | None:
        userprofile = os.environ.get("USERPROFILE", "")
        home = os.environ.get("HOME", "") or userprofile
        candidates: list[str] = []
        if userprofile:
            candidates.append(os.path.join(userprofile, "Documents", "WeChat Files", wxid))
        if home:
            candidates.append(os.path.join(home, "Documents", "xwechat_files", wxid))
        for p in candidates:
            if os.path.isdir(p):
                return p
        return None

    def _iter_sqlite_paths(self, root: str) -> list[str]:
        paths: list[str] = []
        for base, _dirs, files in os.walk(root):
            for name in files:
                if name.lower().endswith(".db"):
                    paths.append(os.path.join(base, name))
        return paths

    def _sqlite_table_exists(self, db_path: str, table: str) -> bool:
        try:
            con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
            try:
                cur = con.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table,))
                return cur.fetchone() is not None
            finally:
                con.close()
        except Exception:
            return False

    def _sqlite_has_rows(self, db_path: str, table: str, row_limit: int = 1) -> bool:
        try:
            con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
            try:
                cur = con.execute(f"SELECT 1 FROM {table} LIMIT {row_limit}")
                return cur.fetchone() is not None
            finally:
                con.close()
        except Exception:
            return False

    def _probe_account_has_sessions(self, wxid: str) -> bool:
        """探测账号是否存在可用会话：
        - 在 Profile 下寻找消息库（如 Msg/MSG*.db 或包含 Message/Conversation 表的任意库）
        - 任意库命中且有行即认为可用
        """
        profile = self._get_profile_root(wxid)
        if not profile:
            return False
        # 优先 Msg 目录
        msg_dir = os.path.join(profile, "Msg")
        dbs: list[str] = []
        if os.path.isdir(msg_dir):
            dbs.extend(self._iter_sqlite_paths(msg_dir))
        # 兜底：全目录扫描小心性能（已限制为 .db）
        if not dbs:
            dbs.extend(self._iter_sqlite_paths(profile))
        for db in dbs:
            if self._sqlite_table_exists(db, "Message") and self._sqlite_has_rows(db, "Message"):
                return True
            if self._sqlite_table_exists(db, "Conversation") and self._sqlite_has_rows(db, "Conversation"):
                return True
        return False

    def _resolve_display_name(self, wxid: str, username: str) -> str:
        """根据联系人/群信息解析会话显示名称：
        优先级：remark > nickname > name > username；群聊优先群名称。
        若找不到任何信息，返回 username。
        """
        profile = self._get_profile_root(wxid)
        if not profile:
            return username
        dbs = self._iter_sqlite_paths(profile)
        # 常见联系人表名集合（不同版本可能不同）
        contact_like = ("Contact", "Contacts", "contact")
        group_like = ("ChatRoom", "Chatroom", "chatroom")

        def select_first_name(db_path: str) -> str | None:
            try:
                con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
                try:
                    cur = con.execute("SELECT name FROM sqlite_master WHERE type='table'")
                    tables = {r[0] for r in cur.fetchall()}
                    # 群聊名称
                    for t in group_like:
                        if t in tables:
                            try:
                                cur = con.execute(f"SELECT name FROM {t} WHERE username=? LIMIT 1", (username,))
                                r = cur.fetchone()
                                if r and r[0]:
                                    return str(r[0])
                            except Exception:
                                pass
                    # 联系人名称
                    for t in contact_like:
                        if t in tables:
                            # 尝试不同列名
                            for cols in (
                                    ("remark", "nickname", "name", "username"),
                                    ("Remark", "Nickname", "Name", "UserName"),
                            ):
                                try:
                                    sql = f"SELECT {', '.join(cols)} FROM {t} WHERE username=? LIMIT 1"
                                    cur = con.execute(sql, (username,))
                                    r = cur.fetchone()
                                    if r:
                                        vals = [str(x) for x in r if isinstance(x, (str, bytes)) and x]
                                        # 依优先级取第一个非空
                                        for val in vals:
                                            if val and val.lower() != username.lower():
                                                return val
                                        return username
                                except Exception:
                                    continue
                finally:
                    con.close()
            except Exception:
                return None
            return None

        for db in dbs:
            name = select_first_name(db)
            if name:
                return name
        return username


def main() -> None:
    if os.name != "nt":
        print("仅支持 Windows 平台。")
        sys.exit(1)
    # 简易命令行：python main.py begin 2 | python main.py end
    svc = WeChatKeyTool()
    args = sys.argv[1:]
    if not args:
        print(json.dumps({"status": "error", "message": "用法: begin <count> | end"}, ensure_ascii=False))
        return
    cmd = args[0].lower()
    if cmd == "begin":
        try:
            count = int(args[1]) if len(args) > 1 else 1
        except Exception:
            count = 1
        res = svc.begin_key_extraction(count)
        print(json.dumps(res, ensure_ascii=False))
    elif cmd == "end":
        res = svc.end_key_extraction()
        print(json.dumps(res, ensure_ascii=False))
    else:
        print(json.dumps({"status": "error", "message": "未知命令"}, ensure_ascii=False))


if __name__ == "__main__":
    main()


